'''
All Rack & Card need to register here before check-in.

It is a master and records identification item and assign identical QR Code.

Applicable for Rack & Card
'''
import calendar
from datetime import datetime as DateTime
from typing import Tuple, Union

from dateutil.relativedelta import relativedelta
from django.db import transaction
from django.db.models import Q
from django.db.models.query import QuerySet
from backend.cews.services import LogService
from backend.cews.models import StockItemEDoc
from backend.cews.models import PartsEDoc
from backend.cews.models import VersionMaster

import backend.cews.services.StockItemService as StockItemService
from backend.cews.services import PartsService
from backend.cews.models import (Asset, EquipmentLevelRelation, Parts,
                                 PartsReplacement, PartVersion,
                                 RepairOrderPartReplacement, StockItemRelation,
                                 StockItemReplacement, System, Workgroup,
                                 RepairOrderPart, Log, PartsImprovementWork,
                                 StockItemImprovementWork)

from .GeneralService import gen_qr_code
import json

def save_part(
    stock_code: str,
    serial_no: str,
    system: str,
    car: str,
    workgroup: str,
    short_description: str,
    description: str,
    date_code: str,
    sw_ver: str,
    hw_ver: str,
    equipment_class: str,
    lv1: str,
    lv2: str,
    lv3: str,
    lv4: str,
    lv5: str = "",
    asset_id: str = "",
    parent_part_stock_code: str = "",
    parent_part_serial_number: str = "",
    obsolescence: int = 0,
    request:str="",
    stock_item_regular_cycle_month: int=36,
) -> Tuple["Parts | None", "Parts | None"]:
    '''
    If the stock code does not exist on Master,
    save a copy to stock code master as a initial master data.

    Otherwise, save itself dataset.
    '''

    part: "Parts | None" = None
    parent_part: "Parts | None" = None

    # check if the same level on stock item table.
    
    with transaction.atomic():
        if not check_part_exist(stock_code, serial_no):
            if not StockItemService.check_stock_item_exist(stock_code):
                _, _ = StockItemService.save_stock_item(
                    stock_code,
                    car,
                    workgroup,
                    system,
                    short_description,
                    description,
                    date_code,
                    sw_ver,
                    hw_ver,
                    obsolescence,
                    equipment_class,
                    lv1,
                    lv2,
                    lv3,
                    lv4,
                    lv5,
                    asset_id,
                    request,
                )

            if not check_part_has_the_same_level(stock_code, lv4, lv5):
                raise ValueError("Level is not the same wtih stock item.")
            part = Parts(
                stock_code=stock_code,
                serial_no=serial_no,
                system=system,
                car=car,
                workgroup=workgroup,
                description=short_description,
                detail_description=description,
                date_code=date_code,
                sw_ver=sw_ver,
                hw_ver=hw_ver,
                asset_id=asset_id,
                equipment_class=equipment_class,
                lv1=lv1,
                lv2=lv2,
                lv3=lv3,
                lv4=lv4,
                lv5=lv5,
                obsolescence=obsolescence,
                stock_item_regular_cycle_month=stock_item_regular_cycle_month,
            )
            if lv5 == '':
                new_log = Log(
                    username=request.user.username,
                    log_type=Log.LogType.STOCKMASTER,
                    category_type=Log.CategoryType.STOCK,
                    action_type=Log.ActionType.CREATE,
                    action='create_stock_item',
                    payload= json.dumps(request.data),
                    description=f"Part: {part.stock_code}, SN: {part.serial_no}, is created",
                    repair_order=0,
                    parent_stock_code='',
                    parent_part='',
                    part=part.serial_no,
                    stock_code=part.stock_code
                )
            else:
                new_log = Log(
                    username=request.user.username,
                    log_type=Log.LogType.STOCKMASTER,
                    category_type=Log.CategoryType.STOCK,
                    action_type=Log.ActionType.CREATE,
                    action='create_stock_item',
                    payload= json.dumps(request.data),
                    description=f"Part: {part.stock_code}, SN: {part.serial_no}, is created",
                    repair_order=0,
                    parent_stock_code=part.stock_code,
                    parent_part=part.serial_no,
                    part='',
                    stock_code=''
                )

            part.save()
            new_log.save()
        else:
            # part = update_part(
            #     get_part(stock_code, serial_no)[0],
            #     system,
            #     car,
            #     workgroup,
            #     short_description,
            #     description,
            #     date_code,
            #     sw_ver,
            #     hw_ver,
            #     asset_id,
            #     equipment_class,
            #     lv1,
            #     lv2,
            #     lv3,
            #     lv4,
            #     lv5
            # )
            
            raise ValueError("Duplicated stock code and serial number.")

        create_part_replacement(part)

        if lv5 != "" \
            and parent_part_stock_code != "" \
                and parent_part_serial_number != "":

            if check_part_exist(
                parent_part_stock_code,
                parent_part_serial_number
            ):
                parent_part = get_part(
                    parent_part_stock_code,
                    parent_part_serial_number
                )[0]

                part.parent_part = parent_part
                part.save()

    return part, parent_part


def create_part_replacement(part):
    stock_item_replacements = StockItemReplacement.objects.filter(
        stock_item__stock_code=part.stock_code,
        deleted=False
    )
    
    for replacement in stock_item_replacements:

        if not PartsReplacement.objects.filter(
            parts__stock_code=part.stock_code,
            parts__serial_no=part.serial_no,
            component=replacement.component,
            deleted=False
        ).exists():

            new_part_replacement = PartsReplacement()
            new_part_replacement.parts = part
            new_part_replacement.component = replacement.component
            new_part_replacement.period = replacement.period
            new_part_replacement.last_replaced = None
            if part.date_code != "":
                new_part_replacement.last_replaced = \
                    DateTime.strptime(part.date_code, "%Y/%m")

            with transaction.atomic():
                new_part_replacement.save()


def check_part_has_the_same_level(stock_code, lv4, lv5):
    stock_item = StockItemService.get_stock_item(stock_code)
    if ((lv4 != "" and lv5 == "") == stock_item.is_parent) \
        or ((lv4 != "" and lv5 != "") == (not stock_item.is_parent)):
            return True
    
    return False


def create_qr_code(
    part: Parts
) -> bytes:
    return gen_qr_code({
        "CAT": part.stock_code,
        "SN": part.serial_no
    })


def update_part(
    current_part,
    system,
    car,
    workgroup,
    short_description,
    description,
    date_code,
    sw_ver,
    hw_ver,
    asset_id,
    equipment_class,
    lv1,
    lv2,
    lv3,
    lv4,
    lv5
) -> Parts:
    with transaction.atomic():
        current_part.system = system
        current_part.car = car
        current_part.workgroup = workgroup
        current_part.description = short_description
        current_part.description = description
        current_part.date_code = date_code
        current_part.sw_ver = sw_ver
        current_part.hw_ver = hw_ver
        current_part.asset_id = asset_id
        current_part.equipment_class = equipment_class
        current_part.lv1 = lv1
        current_part.lv2 = lv2
        current_part.lv3 = lv3
        current_part.lv4 = lv4
        current_part.lv5 = lv5
        current_part.save()

    return current_part


def get_part(
    stock_code: str,
    serial_no: str = ""
) -> QuerySet[Parts]:
    '''
    Unique key: Stock Code + Serial Code which not changeable after created.
    '''
    
    query = Q()
    query.add(Q(deleted=False), Q.AND)
    if ("*" in stock_code) and (len(stock_code) != 1):
        query.add(Q(stock_code__icontains=stock_code.replace("*", "")), Q.AND)
    elif (stock_code != "") :
        query.add(Q(stock_code__iexact=stock_code), Q.AND)

    if ("*" in serial_no) and (len(serial_no) != 1):
        query.add(Q(serial_no__icontains=serial_no.replace("*", "")), Q.AND)
    elif (serial_no != "") :
        query.add(Q(serial_no__iexact=serial_no), Q.AND)
    return Parts.objects.filter(query)


def check_part_exist(
    stock_code: str,
    serial_number: str
) -> bool:
    '''
    Unique key: Stock Code + Serial Code which not changeable after created.
    '''
    return Parts.objects.filter(
        stock_code=stock_code,
        serial_no=serial_number,
        deleted=False
    ).exists()


def get_child_parts(
    parent_stock_code: str,
    parent_serial_no: str,
    sort_desc=False
) -> QuerySet[Parts]:
    sorting_key = "sequence"
    if sort_desc:
        sorting_key = "-" + sorting_key

    # Parent Part:
    # parent_part = None
    # stock_code = 1
    # serial_no = 1234
    # ...
    #
    # Child Part 1:
    # parent_part = (1, 1234)
    # stock_code = 55
    # ...
    # Child Part 2:
    # parent_part = (1, 1234)
    # stock_code = 784
    # ...
    # if search by stock code = 1, will return [Parts(55), Parts(784)].
    return Parts.objects.filter(
        parent_part__stock_code=parent_stock_code,
        parent_part__serial_no=parent_serial_no,
        deleted=False
    ).order_by(sorting_key)


def get_hard_time_replacement(
    stock_code: str,
    serial_no: str
):
    '''
    Tab – Hard Time replacement

    It is a listing to show the replacement task which is pending to do.

    Each item has its own component replacement list.

    There are two types of replacement task lists on each parent and child.

    Regular replacement list.

    Source from Stock Code Master

    maintenance_history One-time off replacement list.

    Source from self-data set

    Use Purchase month or registration date time or last replacement month
    as starting point to calculate the next replace time.

    The list will apply different colors according to the next replace time.

    Listing
    Replacement name, last replace month/year, next replace month/year.
    The next replace month/year is suit for regular replacement only.
    '''
    result = []
    # parent and chidren stock item replacements
    stock_item_replacements = \
        StockItemService.get_hard_time_replacement(stock_code)

    for stock_item_replacement in stock_item_replacements:
        if stock_item_replacement is not None:
            stock_item_replacement_dict = {}
            stock_item_replacement_dict["id"] = \
                stock_item_replacement.id

            stock_item_replacement_dict["stock_code"] = \
                stock_item_replacement.stock_item.stock_code
            
            stock_item_replacement_dict["description"] = \
                stock_item_replacement.stock_item.description

            stock_item_replacement_dict["serial_no"] = ""
            stock_item_replacement_dict["last_replaced"] = None
            stock_item_replacement_dict["next_replace"] = None
            
            
            # determine if the replacement task is one time only.
            stock_item_replacement_dict["one_time"] = stock_item_replacement.one_time
            stock_item_replacement_dict["component"] = \
                stock_item_replacement.component
            
            try:
                # parent
                part = Parts.objects.get(stock_code=stock_code, serial_no=serial_no, deleted=False)
                create_part_replacement(part)


                stock_item_replacement_dict = {}
                parent_replacement_task = PartsReplacement.objects.get(
                    parts__stock_code=stock_item_replacement.stock_item.stock_code,
                    parts__serial_no=serial_no,
                    component=stock_item_replacement.component,
                    deleted=False
                )

                stock_item_replacement_dict["id"] = \
                    parent_replacement_task.id
                stock_item_replacement_dict["stock_code"] = \
                    parent_replacement_task.parts.stock_code
                
                stock_item_replacement_dict["serial_no"] = \
                    parent_replacement_task.parts.serial_no
                
                stock_item_replacement_dict["description"] = \
                    parent_replacement_task.parts.description
                
                stock_item_replacement_dict["period"] = \
                    parent_replacement_task.period
                    
                stock_item_replacement_dict["last_replaced"] = \
                    parent_replacement_task.last_replaced
                    
                stock_item_replacement_dict["next_replace"] = \
                    parent_replacement_task.next_replace
                
                stock_item_replacement_dict["diff"] = \
                    parent_replacement_task.diff
                
                stock_item_replacement_dict["is_expired"] = \
                    parent_replacement_task.is_expired
                
                stock_item_replacement_dict["is_critical"] = \
                    parent_replacement_task.is_critical
                
                stock_item_replacement_dict["component"] = \
                    parent_replacement_task.component
                
                result.append(stock_item_replacement_dict)
                continue
            except PartsReplacement.DoesNotExist as ex:
                print(ex)
            
            try:
                # child
                

                child_replacement_tasks = PartsReplacement.objects.filter(
                    parts__parent_part__stock_code=stock_code,
                    parts__parent_part__serial_no=serial_no,
                    component=stock_item_replacement.component,
                    deleted=False
                )
                for child_replacement_task in child_replacement_tasks:
                    stock_item_replacement_child_dict = {}

                    create_part_replacement(child_replacement_task.parts)
                    # create_part_replacement_if_not_exist(
                    # child_replacement_task.parts.stock_code,
                    # child_replacement_task.parts.serial_no,
                    # child_replacement_task.component)

                    stock_item_replacement_child_dict["id"] = \
                        child_replacement_task.id

                    stock_item_replacement_child_dict["stock_code"] = \
                        child_replacement_task.parts.stock_code
                        
                    stock_item_replacement_child_dict["period"] = \
                        child_replacement_task.period
                    
                    stock_item_replacement_child_dict["description"] = \
                        child_replacement_task.parts.description

                    stock_item_replacement_child_dict["serial_no"] = \
                        child_replacement_task.parts.serial_no
                    
                    stock_item_replacement_child_dict["last_replaced"] = \
                        child_replacement_task.last_replaced
                    
                    stock_item_replacement_child_dict["next_replace"] = \
                        child_replacement_task.next_replace
                    
                    stock_item_replacement_child_dict["diff"] = \
                        child_replacement_task.diff
                    
                    stock_item_replacement_child_dict["is_expired"] = \
                        child_replacement_task.is_expired
                    
                    stock_item_replacement_child_dict["is_critical"] = \
                        child_replacement_task.is_critical
                    
                    stock_item_replacement_child_dict["component"] = \
                        child_replacement_task.component
                    
                    """ print(stock_item_replacement_child_dict) """
                    result.append(stock_item_replacement_child_dict)
                
            except PartsReplacement.DoesNotExist as ex:
                print(ex)
    print(result)
    return result


def get_replacement_task_history(
    stock_code, serial_no, component
):
    return RepairOrderPartReplacement.objects.filter(
        repair_order_part__stock_code=stock_code,
        repair_order_part__serial_no=serial_no,
        component=component,
        deleted=False
    )


def get_parts_version(stock_code, serial_number):
    return PartVersion.objects.filter(
        stock_code__stock_code=stock_code,
        stock_code__serial_no=serial_number,
        deleted=False
    )


def get_parts_history(stock_code, serial_number, count=0):
    if count == 0:
        return Parts.objects.filter(
            stock_code=stock_code,
            serial_no=serial_number,
            deleted=False
        ).order_by("-last_check_in")

    return Parts.objects.filter(
        stock_code=stock_code,
        serial_no=serial_number,
        deleted=False
    ).order_by("-last_check_in")[:count]


# def update_check_in_status(stock_code, serial_number):
#     current_part = Parts.objects.get(
#         stock_code=stock_code,
#         serial_no=serial_number
#     )

#     with transaction.atomic():
#         current_part.check_in_status = \
#             Parts.CheckInStatusType.CHECKED_IN

#         current_part.last_check_in = DateTime.now()
#         current_part.save()

#     return current_part


# def update_check_out_status(stock_code, serial_number):
#     current_part = Parts.objects.get(
#         stock_code=stock_code,
#         serial_no=serial_number
#     )

#     with transaction.atomic():
#         current_part.check_in_status = \
#             Parts.CheckInStatusType.CHECKED_OUT

#         current_part.last_check_out = DateTime.now()
#         current_part.save()

#     return current_part


# def get_check_in_part(stock_code, serial_no):
#     if serial_no == "":
#         return Parts.objects.filter(
#             serial_no=serial_no,
#             check_in_status=Parts.CheckInStatusType.CHECKED_IN
#         )

#     return Parts.objects.filter(
#         stock_code=stock_code,
#         serial_no=serial_no,
#         check_in_status=Parts.CheckInStatusType.CHECKED_IN
#     )


def remove_part_child(
    stock_code,
    serial_no,
    parent_stock_code,
    parent_serial_no,
    request,
    action=''
):
    if check_part_exist(stock_code, serial_no) \
            and check_part_exist(
                parent_stock_code,
                parent_serial_no
            ):
        check_already_in_maintenance(parent_stock_code, parent_serial_no, stock_code, serial_no)
        child_part = get_part(stock_code, serial_no)[0]
        child_part.parent_part = None
        child_part.sequence = 0

        description = f"Part, SC: {stock_code}, SN: {serial_no} {action} defit from Parent, SC: {parent_stock_code}, SN: {parent_serial_no}"
        new_log = Log(
            username=request.user.username,
            log_type=Log.LogType.PARTMASTER,
            category_type=Log.CategoryType.PART,
            action_type=Log.ActionType.DEFITMENT,
            action='remove_part_child',
            payload= json.dumps(request.data),
            description=description,
            repair_order=0,
            parent_stock_code=parent_stock_code,
            parent_part=parent_serial_no,
            part=child_part.serial_no,
            stock_code=child_part.stock_code
        )
        new_log.save()

        with transaction.atomic():
            child_part.save()
            return True
    raise ValueError('Part not found')
    # return False


def assign_part_child(
    stock_code,
    serial_no,
    parent_stock_code,
    parent_serial_no,
    sequence,
    request
):
    if check_part_exist(stock_code, serial_no) \
            and check_part_exist(
                parent_stock_code,
                parent_serial_no
            ):
        ## system update can assign duplicate stock code
        # if new child and old child use the same stock code, 
        # defit old child from current parent
        #old_children = get_part(stock_code).exclude(serial_no=serial_no)
        #for old_child in old_children:
        #    if old_child.parent_part is not None:
        #        if old_child.parent_part.stock_code == parent_stock_code \
        #                and old_child.parent_part.serial_no \
        #                == parent_serial_no:
        #
        #            remove_part_child(
        #                old_child.stock_code,
        #                old_child.serial_no,
        #                old_child.parent_part.stock_code,
        #                old_child.parent_part.serial_no
        #            )
                # defit if the child is fit in parent
        check_already_in_maintenance(parent_stock_code, parent_serial_no, stock_code, serial_no)
        pp = Parts.objects.get(stock_code=parent_stock_code,serial_no=parent_serial_no, deleted=False)
       
        if Parts.objects.filter(parent_part=pp,sequence=sequence).exists():
            old_p = Parts.objects.get(parent_part=pp,sequence=sequence)
            remove_part_child(
                old_p.stock_code,
                old_p.serial_no,
                pp.stock_code,
                pp.serial_no,
                request,
                'auto'
            )

        # if parent exists in new child, defit it original parent.
        child_part = get_part(stock_code, serial_no)[0]
        if child_part.parent_part is not None:
            # defitment of new child
            remove_part_child(
                child_part.stock_code,
                child_part.serial_no,
                child_part.parent_part.stock_code,
                child_part.parent_part.serial_no,
                request,
                'auto'
            )
        

        

            # raise ValueError('child part parent part is not none')
            # return None

        stock_item_child = StockItemService.get_stock_item(stock_code)
        stock_item_parent = StockItemService.get_stock_item(parent_stock_code)
        # if parent_stock_code \
        #         != StockItemRelation.objects.filter(
        #             child=stock_item_child
        #         ).select_related('parent').first().parent.stock_code:

        #     return None

        relation = StockItemRelation.objects.select_related('parent').filter(
            child=stock_item_child,
            parent=stock_item_parent,
            sequence=sequence
        )

        if not relation:
            raise ValueError('No parent found')

        # if relation.parent.stock_code != parent_stock_code:
        #     raise ValueError('Parent stock incorrect')

        parent_part = get_part(
            parent_stock_code,
            parent_serial_no
        )[0]

        child_part.parent_part = parent_part
        #child_part.sequence = \
        #    len(
        #        get_child_parts(
        #            parent_stock_code,
        #            parent_serial_no
        #        )
        #    ) + 1
        child_part.sequence = sequence

        description = f"Part, SC: {stock_code}, SN: {serial_no} fit to Parent, SC: {parent_stock_code}, SN: {parent_serial_no}"
        new_log = Log(
            username=request.user.username,
            log_type=Log.LogType.PARTMASTER,
            category_type=Log.CategoryType.PART,
            action_type=Log.ActionType.FITMENT,
            action='assign_part_child',
            payload= json.dumps(request.data),
            description=description,
            repair_order=0,
            parent_stock_code=parent_stock_code,
            parent_part=parent_serial_no,
            part=serial_no,
            stock_code=stock_code
        )

        with transaction.atomic():
            child_part.save()
            new_log.save()
            return child_part
    raise ValueError('Part not exisit')
    return None


def reorder_part_child(serial_no_sequence_dict):
    '''
    {
        serial_no: sequence
    }
    '''
    new_sequence = []
    part_childs = Parts.objects.filter(
        serial_no__in=list(serial_no_sequence_dict.keys()),
        deleted=False
    )

    if len(part_childs) == 0:
        return False

    for (serial_no, sequence) in serial_no_sequence_dict.items():
        part_child = part_childs.get(serial_no=serial_no)
        part_child.sequence = sequence
        new_sequence.append(part_child)

    with transaction.atomic():
        Parts.objects.bulk_update(new_sequence, ("sequence",))
        return True

    return False


def get_all_lv5_stock_code():
    return Parts.objects.filter(
        deleted=False
    ).exclude(
        lv5__isnull=True
    ).exclude(
        lv5__exact=''
    )


def get_level_options():
    return Asset.objects.all()


def get_system_options():
    return System.objects.all()


def get_workgroup_options():
    return Workgroup.objects.all()


def get_equipment_level(equipment_class):
    return EquipmentLevelRelation.objects.filter(
        equipment__equipment_class=equipment_class
    )


def update_date_code(stock_code, serial_no, date_code):
    current_part_replacement_updates = []
    current_part_replacements = PartsReplacement.objects.filter(
        parts__stock_code=stock_code,
        parts__serial_no=serial_no,
        deleted=False
    )
    
    for current_part_replacement in current_part_replacements:
        if date_code != "":
            current_part_replacement.last_replaced = \
                DateTime.strptime(date_code, "%Y/%m")
        
        current_part_replacement_updates.append(
            current_part_replacement
        )
    
    with transaction.atomic():
        PartsReplacement.objects.bulk_update(
            current_part_replacement_updates,
            ("last_replaced",)
        )
        
        return True
    
    return False

def check_all_status(stock_code, serial_no):
    status = {
        "part_detail":{
            "outstanding":0,
            "message":[]
        },
        "version":{
            "sw":{
                "outstanding":0,
                "message":[],
            },
            "hw":{
                "outstanding":0,
                "message":[],
            }
        },
        "edoc":{
            "outstanding":0,
            "message":[],
        },
        "improvement_work":{
            "outstanding":0,
            "message":[],
        },
        "hard_time_replacement":{
            "outstanding":0,
            "message":[],
        },
    }

    if stock_code and serial_no is not None:
        #part = Parts.objects.get(stock_code=stock_code, serial_no=serial_no)

        status['part_detail'] = check_part_detail_status(stock_code, serial_no)

        status['version'] = check_part_version_status(stock_code, serial_no)

        status['edoc'] = check_part_edoc_status(stock_code, serial_no)

        status['improvement_work'] = check_part_improvement_work_status(stock_code, serial_no)

        status['hard_time_replacement'] = check_part_hard_time_replacement_status(stock_code, serial_no)

    return status

def check_part_detail_status(stock_code, serial_no):
    ## TODO check fitment?
    outstanding = 0
    messages = []
    childs = get_child_parts(stock_code, serial_no)
    if childs is not None:
        for child in childs:
            if child.last_status == 'COMPLETE*':
                outstanding += 1
                messages.append(f'{child.stock_code} {child.serial_no} is not complete in last order.')
    
    
    return {
        'outstanding':outstanding,
        'message':messages,
    }


def check_part_version_status(stock_code, serial_no):
    
    def check_parts_version(parts:Parts):
    
        stock_code = parts.stock_code
        versions = VersionMaster.objects.filter(stock_code=stock_code, deleted=False)
        
        sw_vers = [v.sw_ver.replace(" ", "") for v in versions ]
        hw_vers = [v.hw_ver.replace(" ", "") for v in versions ]
        
        sw_result = {'outstanding':0,'message':[]}
        hw_result = {'outstanding':0,'message':[]}    
        #result = {'outstanding':0,'message':[]}
        has_sw = False
        has_hw = False
        part_sw = parts.sw_ver.split(',')
        part_hw = parts.hw_ver.split(',')

        for sw in part_sw:
            if sw.replace(" ", "") in sw_vers:
                has_sw = True
                break

        # if '' in sw_vers:
        #     has_sw = True
        if has_sw is False:
            sw_result['outstanding'] += 1
            sw_result['message'].append(f'Software version of {parts.serial_no} is not latest.')
                
        for hw in part_hw:
            if hw.replace(" ", "") in hw_vers:
                has_hw = True
                break
        
        # if '' in hw_vers:
        #     has_hw = True
        if has_hw is False:
            hw_result['outstanding'] += 1
            hw_result['message'].append(f'Hardware version of {parts.serial_no} is not latest.')


        return {"sw":sw_result, "hw":hw_result}
    
    parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no,deleted=False)
    child_parts = Parts.objects.filter(parent_part=parts).filter(deleted=False)
    
    sw_outstanding = 0
    hw_outstanding = 0
    sw_message = []
    hw_message = []
    
    res = check_parts_version(parts)
    
    sw_outstanding += res['sw']['outstanding']
    sw_message.extend(res['sw']['message'])
    hw_outstanding += res['hw']['outstanding']
    hw_message.extend(res['hw']['message'])
    
    for child in child_parts:
        res = check_parts_version(child)
    
        sw_outstanding += res['sw']['outstanding']
        sw_message.extend(res['sw']['message'])

        hw_outstanding += res['hw']['outstanding']
        hw_message.extend(res['hw']['message'])
    
    return {
        "sw":{
            'outstanding':sw_outstanding,
            'message':sw_message
        },
        "hw":{
            'outstanding':hw_outstanding,
            'message':hw_message
        }       
    }
    
# def check_part_version_status(stock_code, serial_no):
#     outstanding = 0
#     messages = []

#     stock_codes = []
#     stock_codes.append(stock_code)

#     childs = get_child_parts(stock_code, serial_no)
#     if childs is not None:
#         for child in childs:
#             stock_codes.append(child.stock_code)

#     ver_dict = VersionMaster.get_version_dict(stock_codes)
#     parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no)
#     ## check self 
#     part_version = PartVersion.objects.filter(
#         stock_code=stock_code,
#         parts=parts,
#         deleted=False
#     ).latest('id')

#     latest_ver = ver_dict.get(stock_code)
#     if not latest_ver:
#         outstanding += 1
#         messages.append(f'Version of {stock_code} is not found.')

#     if isinstance(latest_ver, list):
#         sw_vers = [l.sw_ver for l in latest_ver]
#         hw_vers = [l.hw_ver for l in latest_ver] 

#         if not part_version.sw_ver in sw_vers:
#             outstanding += 1
#             messages.append(f'Software version of {part_version.stock_code} is not latest.')
            
#         if not part_version.hw_ver in hw_vers:
#             outstanding += 1
#             messages.append(f'Hardware version of {part_version.stock_code} is not latest.')

#     ##check child if have childs
#     if childs is not None:
#         for child in childs:
#             parts = Parts.objects.get(stock_code=child.stock_code, serial_no=child.serial_no)
#             part_version = PartVersion.objects.filter(
#                 stock_code=child.stock_code,
#                 parts=parts,
#                 deleted=False
#             ).latest('id')
#             latest_ver = ver_dict.get(child.stock_code)
#             if not latest_ver:
#                 outstanding += 1
#                 messages.append(f'Version of {stock_code} is not found.')

#             if isinstance(latest_ver, list):
#                 sw_vers = [l.sw_ver for l in latest_ver]
#                 hw_vers = [l.hw_ver for l in latest_ver] 
 
#                 if not part_version.sw_ver in sw_vers:
#                     outstanding += 1
#                     messages.append(f'Software version {part_version.sw_ver} of {part_version.stock_code} is not latest.')
                    
#                 if not part_version.hw_ver in hw_vers:
#                     outstanding += 1
#                     messages.append(f'Hardware version {part_version.hw_ver} of {part_version.stock_code} is not latest.')

#     return {
#         'outstanding':outstanding,
#         'message':messages,
#     }

def check_part_edoc_status(stock_code, serial_no):
    def check_parts_edoc(parts:Parts):
        
        outstanding = 0
        message = []
        
        ## stock_code
        stock_code = parts.stock_code
        
        ## get parts edoc
        parts_edocs = PartsEDoc.objects.filter(parts=parts)
        parts_edocs_dict = {p.stock_code+p.doc_name+p.version:p for p in parts_edocs}
        
        ## get stock item edoc
        stock_item_edocs = StockItemEDoc.objects.filter(stock_item__stock_code=stock_code,invalid=0)
        
        for stock_item_edoc in stock_item_edocs:
            key = stock_item_edoc.stock_code+stock_item_edoc.doc_name+stock_item_edoc.version        
            parts_edoc = parts_edocs_dict.get(key,False)
            if parts_edoc is False:
                outstanding += 1
                message.append(f"{stock_item_edoc.stock_code} {stock_item_edoc.doc_name} {stock_item_edoc.version} is not complete.") 
        
        return outstanding,message
    
    parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no, deleted=False)
    child_parts = Parts.objects.filter(parent_part=parts).filter(deleted=False)
    result = {
        'outstanding':0,
        'message':[]
    }
    
    outstanding,message = check_parts_edoc(parts)
    
    result['outstanding'] += outstanding
    result['message'].extend(message)
    
    for child in child_parts:
        outstanding,message = check_parts_edoc(child)
        result['outstanding'] += outstanding
        result['message'].extend(message)
    
    return result


def check_part_improvement_work_status(stock_code, serial_no):
    def check_parts_imp_work(parts:Parts):
        
        outstanding = 0
        message = []
        
        ## stock_code
        stock_code = parts.stock_code
        
        ## get parts edoc
        parts_edocs = PartsImprovementWork.objects.filter(parts=parts)
        parts_edocs_dict = {p.stock_code+p.doc_name+p.version:p for p in parts_edocs}
        
        ## get stock item edoc
        stock_item_edocs = StockItemImprovementWork.objects.filter(stock_item__stock_code=stock_code,invalid=0)
        
        for stock_item_edoc in stock_item_edocs:
            key = stock_item_edoc.stock_code+stock_item_edoc.doc_name+stock_item_edoc.version        
            parts_edoc = parts_edocs_dict.get(key,False)
            if parts_edoc is False:
                outstanding += 1
                message.append(f"{stock_item_edoc.stock_code} {stock_item_edoc.doc_name} {stock_item_edoc.version} is not complete.") 
        
        return outstanding,message
    
    parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no, deleted=False)
    child_parts = Parts.objects.filter(parent_part=parts).filter(deleted=False)
    result = {
        'outstanding':0,
        'message':[]
    }
    
    outstanding,message = check_parts_imp_work(parts)
    
    result['outstanding'] += outstanding
    result['message'].extend(message)
    
    for child in child_parts:
        outstanding,message = check_parts_imp_work(child)
        result['outstanding'] += outstanding
        result['message'].extend(message)
    
    return result

    
# def check_part_edoc_status(stock_code, serial_no):
#     outstanding = 0
#     messages = []

#     childs = get_child_parts(stock_code, serial_no)

#     ## check self
#     parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no)
#     stockEdoc = StockItemEDoc.objects.get(stock_code=stock_code, serial_no=serial_no)

#     p_edocs = PartsEDoc.objects.filter(stock_code=stock_code, parts=parts)
        
#     for p_edoc in p_edocs:
#         if p_edoc.complete == 0:
#             outstanding += 1
#             messages.append(f'Edoc not complete of {parts.serial_no}')

#     ## check childs
#     if childs is not None:
#         for child in childs:
#             parts = Parts.objects.get(stock_code=child.stock_code, serial_no=child.serial_no)

#             p_edocs = PartsEDoc.objects.filter(stock_code=child.stock_code, parts=parts)
            
#             for p_edoc in p_edocs:
#                 if p_edoc.complete == 0:
#                     outstanding += 1
#                     messages.append(f'Edoc not complete of {parts.serial_no}')

#     return {
#         'outstanding':outstanding,
#         'message':messages,
#     }
    
def check_part_hard_time_replacement_status(stock_code, serial_no):
    
    def check_parts_hardtime_replacement(parts:Parts):
        outstanding = 0
        message = []
        
        ## get parts repalcement
        parts_replacements = PartsReplacement.objects.filter(parts=parts)
        for parts_replacement in parts_replacements:
            if parts_replacement.is_critical:
                outstanding +=1
                message.append(f'{parts_replacement.component} is critical and not replaced.')

        return outstanding,message
    
    parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no, deleted=False)
    child_parts = Parts.objects.filter(parent_part=parts).filter(deleted=False)
    
    result = {
        'outstanding':0,
        'message':[]
    }
    
    outstanding,message = check_parts_hardtime_replacement(parts)
    result['outstanding'] += outstanding
    result['message'].extend(message)
    
    
    for child in child_parts:
        outstanding,message = check_parts_hardtime_replacement(child)
        result['outstanding'] += outstanding
        result['message'].extend(message)
    
    return result

# def check_part_hard_time_replacement_status(stock_code, serial_no):
#     outstanding = 0
#     messages = []

#     childs = get_child_parts(stock_code, serial_no)

#     ## check self
#     parts = Parts.objects.get(stock_code=stock_code, serial_no=serial_no)

#     p_hts = PartsReplacement.objects.filter(parts=parts)
    
#     for p_ht in p_hts:
#         if p_ht.diff <= 3:
#             outstanding += 1
#             messages.append(f'Hard Time Replacement time of {parts.serial_no} is less than 3 months')

#     ## check childs
#     if childs is not None:
#         for child in childs:
#             parts = Parts.objects.get(stock_code=child.stock_code, serial_no=child.serial_no)

#             p_hts = PartsReplacement.objects.filter(parts=parts)
            
#             for p_ht in p_hts:
#                 if p_ht.diff <= 3:
#                     outstanding += 1
#                     messages.append(f'Hard Time Replacement time of {parts.serial_no} is less than 3 months')

#     return {
#         'outstanding':outstanding,
#         'message':messages,
#     }

def soft_delete_part(id, user):

    if not user.has_perm('cews.delete_parts'):
        raise ValueError('User has no permission to delete parts.')
    
    p = Parts.objects.get(pk=id)

    if RepairOrderPart.objects.filter(parts=p).exists():
        raise ValueError('Part had been created order, cannot delete')

    # p.deleted = True
    if p.parent_part:
        raise ValueError('Part had been fitted, cannot delete')

    prl = PartsReplacement.objects.filter(parts=p, deleted=False)

    for pr in prl:
        # pr.deleted = True
        # pr.save()
        pr.delete()

    pvl = PartVersion.objects.filter(parts=p, deleted=False)

    for pv in pvl:
        # pv.deleted = True
        # pv.save()
        pv.delete()


    pel = Parts.objects.filter(parts=p, deleted=False)

    for pe in pel:
        # pe.deleted = True
        # pe.save()
        pe.delete()
    
    description = f"Part, SC: {p.stock_code}, SN: {p.serial_no} is deleted"


    result = LogService.check_part_is_parent(p)

    new_log = Log(
        username=user,
        log_type=Log.LogType.PARTMASTER,
        category_type=Log.CategoryType.PART,
        action_type=Log.ActionType.DELETE,
        action='delete_part',
        payload= '',
        description=description,
        repair_order=0,
        parent_stock_code=result['parent_stock_code'],
        parent_part=result['parent_serial_no'],
        part=result['serial_no'],
        stock_code=result['stock_code']
    )

    with transaction.atomic():
        new_log.save()
        p.delete()

        return True
    
def check_already_in_maintenance(parent_stock_code, parent_serial_no, stock_code, serial_no):
   
    if RepairOrderPart.objects.filter(stock_code=parent_stock_code,serial_no=parent_serial_no, deleted=False, status="CHECKED IN").exists():
        pp = Parts.objects.get(stock_code=parent_stock_code, serial_no=parent_serial_no, deleted=False)
        raise ValueError(f"{pp.description}, {pp.serial_no} has already in maintenance")
    
    if RepairOrderPart.objects.filter(stock_code=stock_code,serial_no=serial_no, deleted=False, status="CHECKED IN").exists():
        child = Parts.objects.get(stock_code=stock_code, serial_no=serial_no, deleted=False)
        raise ValueError(f"{child.description}, {child.serial_no} has already in maintenance")

# def create_part_replacement_if_not_exist(stock_code, serial_no, component):
#     part = Parts.objects.get(stock_code=stock_code, serial_no=serial_no, deleted=False)

#     PartsService.create_part_replacement(part)
from config.settings.base import STATICFILES_DIRS
import base64
import qrcode
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO


def get_qr_code_image(part_id,qrcode_size,cews_size):
  
    part = Parts.objects.get(pk=part_id, deleted=False)
    qr_text = json.dumps({"CAT": part.stock_code,"SN": part.serial_no})
    text = f"SC: {part.stock_code},SN: {part.serial_no}"
    # sn_text = f"SN: {part.serial_no}"
    image_data = generate_qr_with_logo(qr_text, text, qrcode_size, cews_size)


    encoded_image = base64.b64encode(image_data).decode('utf-8')
    context = {
        'encoded_image': encoded_image,
        'SC' : part.stock_code,
        'SN' : part.serial_no,
        'seq' : part.sequence,
        }
    
    return context
    
    
def generate_qr_with_logo(qr_text, text, qrcode_size, cews_size):
    # Create a QR code instance
    qr = qrcode.QRCode(
        version=3,
        # error_correction=qrcode.constants.ERROR_CORRECT_Q,
        box_size=3,
        border=0,
    )

    # Add data to the QR code
    qr.add_data(qr_text)
    qr.make(fit=True)

    # Generate an image from the QR code
    qr_image = qr.make_image(fill_color="black", back_color="white")
    # qr_image = qr.make_image(back_color=(255, 195, 235), fill_color=(55, 95, 35))
    # print(STATICFILES_DIRS)
    # Load and resize the logo image
    logo_size = (cews_size, cews_size)  # Adjust the size of the logo as needed
    logo = Image.open(f"{STATICFILES_DIRS[0]}/images/svgs/24.png")
    logo = logo.resize(logo_size)
    # print(logo)
    new_size = (qrcode_size, qrcode_size) 
    resized_image = qr_image.resize(new_size)
    # print(resized_image)
    # Calculate the position to place the logo at the center of the QR code
    qr_width, qr_height = resized_image.size
    logo_width, logo_height = logo.size
 
    logo_position = ((qr_width - logo_width) // 2, (qr_height - logo_height) // 2)

    # Paste the logo onto the QR code image
    resized_image.paste(logo, logo_position)

    ## add text in img
    background = Image.new('RGBA', (192, 104), (255,255,255,255))
    draw = ImageDraw.Draw(background)

    arr = text.split(',')
    print(arr)
    count = 0
    font = ImageFont.truetype(f"{STATICFILES_DIRS[0]}/fonts/arial.ttf", size=12)
    for long_str in arr:
        for i in range(0, len(long_str), 13):
            chunk = long_str[i:i+13]
            # qrcode right
            # draw.text((0,5+count),chunk, (0,0,0))
            # qrcode left
            draw.text((100,5+count),chunk, (0,0,0), font=font)
            count += 10
   

    # draw.text((80,35),sn, (0,0,0))

    # qrcode right
    # background.paste(resized_image, (90,5))
    # qrcode left
    background.paste(resized_image, (5,5))

    # Create a buffer to hold the image data
    buffer = BytesIO()
 
    # Convert the image to bytes and save it to the buffer
    # qr_image.save(buffer, format='PNG')
    background.save(buffer, format='PNG')
    image_bytes = buffer.getvalue()

    # Set the buffer's position to the start
    buffer.seek(0)

    return image_bytes